﻿using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;

namespace Heimdallr.IO
{
    /// <summary>
    /// Represents a notification from file system.
    /// </summary>
    public class FileInfoNotification : Notification
    {
        #region Field

        private readonly static NameValueCollection mime = GetDefaultMimeCollection();

        private readonly DateTime time;
        private readonly TimeSpan duration;
        private readonly NotificationLevel level;

        private readonly string filename;
        private readonly Lazy<string> contentType;
        private readonly Lazy<int> contentLength;

        #endregion

        #region Property

        /// <summary>
        /// Gets the time of this notification generated.
        /// </summary>
        public sealed override DateTime Time
        {
            get { return this.time; }
        }

        /// <summary>
        /// Gets the effective duration of this notification.
        /// </summary>
        /// <remarks>
        /// Negative value means the duration is infinity.
        /// </remarks>
        public sealed override TimeSpan Duration
        {
            get { return this.duration; }
        }

        /// <summary>
        /// Gets the level of this notification.
        /// </summary>
        /// <remarks>
        /// The default value is <see cref="NotificationLevel">NotificationLevel.Info</see>.
        /// </remarks>
        public sealed override NotificationLevel Level
        {
            get { return this.level; }
        }

        /// <summary>
        /// Gets the MIME type of this notification.
        /// </summary>
        /// <remarks>The value is typically the MIME encoding of the content of this notification.</remarks>
        public sealed override string ContentType
        {
            get { return this.contentType.Value; }
        }

        /// <summary>
        /// Gets the file length of this notification.
        /// </summary>
        /// <exception cref="InvalidOperationException">Can not get the file length.</exception>
        public sealed override int ContentLength
        {
            get { return this.contentLength.Value; }
        }

        /// <summary>
        /// Gets the full path of the file.
        /// </summary>
        public string FullName
        {
            get { return this.filename; }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance, using specified notification level and filename.
        /// </summary>
        /// <param name="level">Specified notification level.</param>
        /// <param name="filename">Specified filename. This parameter is permitted to specify relative or absolute path information.</param>
        /// <exception cref="ArgumentNullException"><paramref name="filename"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="filename"/> is invalid.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="level"/> is not a valid value.</exception>
        public FileInfoNotification(NotificationLevel level, string filename)
            : this(level, DateTime.Now, TimeSpan.Zero, filename)
        {
        }

        /// <summary>
        /// Initializes a new instance, using specified notification level, time and filename.
        /// </summary>
        /// <param name="level">Specified notification level.</param>
        /// <param name="time">The time of this notification generated.</param>
        /// <param name="filename">Specified filename. This parameter is permitted to specify relative or absolute path information.</param>
        /// <exception cref="ArgumentNullException"><paramref name="filename"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="filename"/> is invalid.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="level"/> is not a valid value.</exception>
        public FileInfoNotification(NotificationLevel level, DateTime time, string filename)
            : this(level, time, TimeSpan.Zero, filename)
        {
        }

        /// <summary>
        /// Initializes a new instance, using specified notification level, time, duration and filename.
        /// </summary>
        /// <param name="level">Specified notification level.</param>
        /// <param name="time">The time of this notification generated.</param>
        /// <param name="duration">The effective duration of this notification. Negative value means the duration is infinity.</param>
        /// <param name="filename">Specified filename. This parameter is permitted to specify relative or absolute path information.</param>
        /// <exception cref="ArgumentNullException"><paramref name="filename"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="filename"/> is invalid.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="level"/> is not a valid value.</exception>
        public FileInfoNotification(NotificationLevel level, DateTime time, TimeSpan duration, string filename)
        {
            if (!Enum.IsDefined(typeof(NotificationLevel), level))
                throw new InvalidEnumArgumentException("level", (int)level, typeof(NotificationLevel));

            this.level = level;
            this.time = time;
            this.duration = duration;
            this.filename = CheckFileNameArgument(filename);

            this.contentType = new Lazy<string>(this.ContentTypeFactory, true);
            this.contentLength = new Lazy<int>(this.ContentLengthFactory, true);
        }

        #endregion

        #region Method

        /// <summary>
        /// Creates a read-only stream instance allows user to read the file.
        /// </summary>
        /// <returns>A read-only stream instance.</returns>
        /// <exception cref="InvalidOperationException">Can not create stream.</exception>
        public sealed override Stream CreateReadOnlyStream()
        {
            return this.CreateReadOnlyStream(65536);
        }

        /// <summary>
        /// Creates a read-only stream instance with specified buffer size allows user to read the file.
        /// </summary>
        /// <param name="bufferSize">The size of the buffer. This value must be greater than zero.</param>
        /// <returns>A read-only stream instance.</returns>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="bufferSize"/> is negative or zero.</exception>
        /// <exception cref="InvalidOperationException">Can not create stream.</exception>
        public sealed override Stream CreateReadOnlyStream(int bufferSize)
        {
            if (bufferSize < 0)
                throw new ArgumentOutOfRangeException("bufferSize", "Value is negative or zero.");

            try
            {
                return new FileStream(this.filename, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize);
            }
            catch (Exception error)
            {
                throw new InvalidOperationException("Can not create stream.", error);
            }
        }

        /// <summary>
        /// Writes the file to a new byte array and returns it.
        /// </summary>
        /// <returns>A new byte array.</returns>
        /// <exception cref="InvalidOperationException">Can not create buffer.</exception>
        public sealed override byte[] GetBuffer()
        {
            try
            {
                return File.ReadAllBytes(this.filename);
            }
            catch (Exception error)
            {
                throw new InvalidOperationException("Can not create buffer.", error);
            }
        }

        #endregion

        #region Lazy Initialization

        private string ContentTypeFactory()
        {
            string extension = Path.GetExtension(this.filename);

            if (string.IsNullOrWhiteSpace(extension))
                return DefaultContentType;

            string result = mime[extension];

            if (string.IsNullOrWhiteSpace(result))
                return DefaultContentType;
            else
                return result;
        }

        private int ContentLengthFactory()
        {
            FileInfo fileInfo = new FileInfo(this.filename);

            try
            {
                return (int)fileInfo.Length;
            }
            catch (Exception error)
            {
                throw new InvalidOperationException("Can not get the file length.", error);
            }
        }

        #endregion

        #region Others

        private static string CheckFileNameArgument(string filename)
        {
            if (filename == null)
                throw new ArgumentNullException("filename");

            try
            {
                if (Path.IsPathRooted(filename))
                    return filename;
                else
                    return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filename);
            }
            catch (Exception error)
            {
                throw new ArgumentException("Invalid path or file name.", "filename", error);
            }
        }

        #endregion
    }
}